Skip to content

feat: add Bitwarden Password Manager provider#166

Open
ashebanow wants to merge 20 commits into
cachix:mainfrom
ashebanow:bitwarden-provider
Open

feat: add Bitwarden Password Manager provider#166
ashebanow wants to merge 20 commits into
cachix:mainfrom
ashebanow:bitwarden-provider

Conversation

@ashebanow

@ashebanow ashebanow commented Jul 17, 2026

Copy link
Copy Markdown

Adds a Bitwarden Password Manager provider (bw://) backed by the official bw CLI.

This resumes the effort from #24 — the renewed discussion in #15 is what inspired picking this work back up. It starts from the Password Manager core of that PR and reworks it for today's codebase.

What's included

  • bw:// provider shelling out to the bw CLI, with vault-wide access to all item types: logins, cards, identities, SSH keys, and secure notes
  • Provider id bw matches the CLI it drives and the cargo feature, mirroring how bws:// is named after the bws CLI
  • Sensible per-type default fields, plus explicit selection via URI query params (bw://?type=login&field=username) and BITWARDEN_DEFAULT_TYPE / BITWARDEN_DEFAULT_FIELD env vars (prefix kept clear of the bw CLI's own BW_* namespace)
  • ref = { item = "...", field = "..." } mapping in secretspec.toml, so secret keys stay valid identifiers while pointing at arbitrarily named vault items
  • Collection/organization addressing (bw://org@collection) and self-hosted servers (?server=)
  • Item creation and update on secretspec set
  • Curated error mapping for common bw failure modes: CLI not installed (with install instructions), not logged in, vault locked; secret values pass via stdin and the session via the environment, never argv
  • bw cargo feature (default-enabled; adds only a base64 dependency since the provider drives the CLI rather than linking an SDK)
  • Docs: provider page (docs/providers/bw.md with 0.16 version notice) and all provider listings per the adding-providers checklist — sidebar, llms.txt description, concepts/reference tables, security table, landing page, quick-start, README — each labeled (0.16+)
  • CHANGELOG entry under Unreleased, unit tests for URI/config parsing
  • tests/bitwarden_integration.sh: end-to-end suite against a real vault covering all five item types, ref mapping, env-var defaults, error paths, and set/update; auto-creates and cleans up its own test items
  • bitwarden-cli added to devenv for integration testing

Changes relative to #24

  • Secrets Manager support dropped — upstream now ships a dedicated bws:// provider, so this PR is Password Manager only
  • Ported to the current ProviderUrl/Address API and provider registration
  • Add Bitwarden provider with comprehensive security enhancements #24's custom timeout machinery, error-message sanitizer, and perf instrumentation were not carried over — the rewrite follows the same straightforward CLI-execution pattern as the 1Password/LastPass providers
  • Integration script hardened: automatic test-item setup/teardown and valid-identifier keys via ref mapping
  • The integration script's header documents a vaultwarden-based path to fully automated, credential-free testing (future work)

Testing

  • cargo test --all green locally (macOS)
  • Integration script run against a real vault: all item types, ref mapping, env-var defaults, error handling, and set/update paths

Notes for reviewers

  • Provider listings are complete per the adding-providers checklist (sidebar, concepts/reference tables, landing page, quick-start, README), all labeled 0.16+
  • Real-world testing so far has focused on a personal vault exercising all five item types; organization/collection layouts have had lighter coverage — extra scrutiny welcome there
  • The integration script's header documents a vaultwarden-based path to fully automated, credential-free testing, left as future work

Refs #15 (inspiration for resuming), supersedes #24.

AI Note: this project's code and documentation were produced with the help of UI. The old pull request was developed with the help of Claude Code and Claude Opus/Sonnet, the additional work for this pull request was done using my custom pi environment and Deepseek v4 Pro/Flash.

ashebanow and others added 16 commits July 17, 2026 15:00
Integrate Bitwarden Password Manager (bw CLI) as a secretspec
provider. The provider supports vault-wide item access across all
Bitwarden item types (Login, Card, Identity, SSH Key, Secure Note)
with smart field extraction, URI-based configuration
(bitwarden://[org@]collection?server=...&type=...&field=...),
and session-based authentication via BW_SESSION.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Upstream now ships a native BWS SDK provider (bws.rs), so the
unified Password Manager + Secrets Manager provider must be reduced
to Password Manager only. Remove the BitwardenService enum,
BWS-specific config fields, execute_bws_command method, and
bws:// scheme handling from register_provider! and TryFrom.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Upstream's Provider trait has evolved from dynamic dispatch with
(project, key, profile) signatures to a static-registration pattern
with Address, ProviderUrl, and ProviderCredentials. Rewrite
BitwardenConfig's TryFrom to accept &ProviderUrl, add
convention_address/supported_coords/with_credentials/uri, and
update get/set to accept Address<'_> parameters resolved via
resolve_coords. Field resolution respects the native address
coordinate first, then BITWARDEN_DEFAULT_FIELD, then config.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Wire the bitwarden module into the provider registry with a bw
feature flag (bw = ["dep:base64"], enabled by default). The base64
dependency encodes JSON for the bw CLI's item create/edit stdin
path. Add four unit tests covering provider creation, collection,
org-collection URI parsing, and bws:// scheme rejection.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
The bitwarden.md page was drafted for a dual-service (bw + bws)
provider. Strip empty code blocks, the Secrets Manager access token
section, and a stray bullet left behind after BWS content removal.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Add .pi/ to .gitignore for pi-agent task output.
The collapsible_if lint in config::migrate_macos_config is a
pre-existing upstream warning surfaced by clippy during this
branch's development.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Replace the manual setup requirements with an auto-provisioning
setup_test_data() function that creates Bitwarden items if they
don't exist, and registers an EXIT trap to clean them up unless
--keep-test-data is passed.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Replace invalid TOML keys ("Deploy SSH Key", etc.) with valid
bw_integration_test_* identifiers and ref = { item, field }
mapping, matching the new Provider API address model.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
The entry landed in the already-released 0.15.0 section during the
rebase; the provider targets the next release.
The provider wraps the `bw` CLI, so the id now matches the tool it
drives and the existing cargo feature, mirroring how the bws provider
is named after the bws CLI. URIs change from bitwarden:// to bw://.
The provider is unreleased, so there is no compatibility impact.

Module, source file, and docs page follow the id (provider::bw,
providers/bw.md). BITWARDEN_DEFAULT_TYPE/FIELD env vars keep their
prefix to stay clear of the bw CLI's own BW_* namespace.
Add the Bitwarden Password Manager provider to every listing the
adding-providers checklist names: sidebar and llms.txt description,
concepts and reference tables (with security row), landing page
metadata/hero/bento (provider count 14 -> 15), quick-start and README
config-init examples, and the rustdoc provider summary. All entries
carry the 0.16+ label; the provider page gains the version notice.

The landing grid's existing plain 'Bitwarden' label becomes 'Bitwarden
Secrets Manager' to disambiguate the two Bitwarden providers.
bw can prompt on stdin (e.g. for the master password when a session is
missing or expired). stdin is null or closed on our invocations so a
prompt could not hang us, but --nointeraction makes bw fail fast with
its own clear error instead of a confusing EOF-related one, which
matters in CI and other headless contexts.
Implements the FUTURE WORK plan from bitwarden_integration.sh:
vaultwarden + caddy TLS proxy (the 2026+ bw CLI refuses plain http
servers, so an internal self-signed cert is required), fixture account
registered via the identity API (vaultwarden_bootstrap.py implements the
client-side registration crypto bw doesn't expose), bw CLI isolated via
BITWARDENCLI_APPDATA_DIR so the developer's real config is untouched.
No repository secrets needed; works on fork PRs.

Verified locally: full bitwarden_integration.sh suite 19/19 PASSED
end-to-end from a cold start (containers, registration, login, suite,
cleanup) on macOS + colima.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CDSY4yJXvof9Am8acxzHf
@bsorescu

Copy link
Copy Markdown
Contributor

Vaultwarden testing done, as promised on #15: the full tests/bitwarden_integration.sh suite passes 19/19 against a disposable Vaultwarden (vaultwarden/server:latest) through the genuine bw CLI 2026.6.0 — reads across all item types (login, card, identity, SSH key, secure note, custom fields), writes (set creating login/card items), and the error-handling cases.

And since you asked about automating it: I've opened ashebanow#2 against your branch implementing the FUTURE WORK plan from the script header — disposable Vaultwarden + fixture account, no repository secrets, fork-PR-friendly. Two gotchas discovered on the way, handled in the harness:

  • the 2026+ bw CLI refuses plain http:// servers outright, so the harness fronts Vaultwarden with a Caddy TLS proxy (internal self-signed cert + NODE_TLS_REJECT_UNAUTHORIZED=0 scoped to the fixture run);
  • bw has no register, so a small vaultwarden_bootstrap.py implements the client-side registration crypto (PBKDF2 → HKDF stretch → EncString type 2 → RSA keypair) against the identity API.

BITWARDENCLI_APPDATA_DIR keeps the whole thing isolated from the developer's real bw config. A GitHub Actions job wiring this up should be a ~20-line follow-up once this lands.

}

// Parse query parameters
for (key, value) in url.query_pairs() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — Enable query parsing in bw-only builds

When consumers build only the advertised feature with cargo check -p secretspec --no-default-features --features bw, this call fails because ProviderUrl::query_pairs is not compiled unless another provider feature or test is enabled. Please include bw in that method’s cfg gate so the feature compiles independently.


// Configure server if specified
if let Some(server) = &self.config.server {
cmd.env("BW_SERVER", server);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — Configure self-hosted servers through a supported bw mechanism

For bw://?server=..., setting BW_SERVER has no effect because the official bw CLI does not read that environment variable; BW_SERVER=https://example.invalid bw status still reports the configured default server. Consequently every advertised self-hosted operation targets the CLI’s existing server instead, so this needs a supported configuration mechanism.

// Use Bitwarden's built-in search to find items matching the key
let mut list_args = vec!["list", "items", "--search", item_name];

// Add organization filter if configured (from config or environment variable)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — Apply the configured collection to vault searches

When bw://collection-id, bw://org@collection, or BITWARDEN_COLLECTION is used, this lookup applies only the organization filter and ignores the collection. If another collection contains the same item name, reads can return its value, and the equivalent unfiltered write search can update it. Please restrict searches with the configured collection ID.

0 => Some(BitwardenFieldType::Text),
1 => Some(BitwardenFieldType::Hidden),
2 => Some(BitwardenFieldType::Boolean),
_ => None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — Accept linked Bitwarden custom fields

Bitwarden defines custom-field type 3 for linked fields, but this parser rejects it. Since set_to_password_manager deserializes every item returned by bw list items, one linked field anywhere in the vault causes every write to abort with Unknown field type: 3; reads of an item containing one also fail.

match field.to_lowercase().as_str() {
"username" => login_data["username"] = serde_json::Value::String(value.to_string()),
"totp" => login_data["totp"] = serde_json::Value::String(value.to_string()),
_ => login_data["password"] = serde_json::Value::String(value.to_string()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — Preserve explicitly named custom fields when creating items

When a new login is created with a documented custom field such as field=api_key, this fallback stores the value in login.password. A subsequent read using the same provider looks for the custom api_key field and returns nothing. Card and Identity creation use the same incorrect fallback pattern for unknown field names.

.map(|s| s.to_string())
.or_else(|| std::env::var("BITWARDEN_DEFAULT_FIELD").ok())
.or_else(|| self.config.default_field.clone())
.unwrap_or_else(|| "password".to_string());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — Choose type-specific defaults when updating existing items

For an existing Secure Note, Card, Identity, or SSH-key item without an explicit field, this always selects password. The updater therefore creates a custom password field while the corresponding getter defaults to notes/value, card number, email, or private key, so set succeeds but a following get returns the old value.


// Look for existing field
for field_obj in fields.iter_mut() {
if field_obj["name"].as_str() == Some(field) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Match custom-field names consistently when updating

Reads match custom-field names case-insensitively, but updates use case-sensitive equality here. With an existing field named API_KEY and a reference to api_key, reading works while setting adds a duplicate field; later reads encounter the original field first and continue returning the stale value.


The `bw` provider integrates with Bitwarden Password Manager for secret management with vault-wide access to all item types.

:::note[Version compatibility]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Advertise the actual target release everywhere

The base already contains the released 0.16.0 section dated 2026-07-17, so this unreleased provider cannot be described as an upcoming 0.16 feature. Please update every new listing to the actual target release, including the homepage entries that currently omit a version entirely; otherwise users of 0.16 are told the unavailable provider is supported.


```toml
# secretspec.toml
[development]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Use valid profile configuration in the provider guide

These tables do not match the project configuration schema: profiles are under [profiles.<name>], and profile provider selection is configured through the profile’s defaults.providers alias list rather than a provider string. Users following this example will get an invalid or misinterpreted secretspec.toml instead of collection-specific profiles.

Comment thread CHANGELOG.md
## [0.16.0] - 2026-07-17

### Added
>>>>>>> upstream/main

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Remove the unresolved changelog merge marker

The literal >>>>>>> upstream/main is a merge artifact and will be published as part of the 0.16 changelog unless removed.

One test per finding (R1 linked-field type 3 poisons writes; R2 named
custom field lost on create; R3 wrong default field on non-login update;
R4 case-sensitive update vs case-insensitive read). Each reports
REPRODUCED/FIXED and the set exits 0 only when all findings are fixed —
opt-in via RUN_REGRESSIONS=1 in the harness until then.

Verified against disposable Vaultwarden: currently 0 fixed / 4 reproduced,
matching the review descriptions verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CDSY4yJXvof9Am8acxzHf
@bsorescu

Copy link
Copy Markdown
Contributor

Following up on the review: the four findings that are reproducible against a live vault are now regression tests in the Vaultwarden harness (added to ashebanow#2) — one test per finding, each reporting REPRODUCED/FIXED, exiting green only when all are fixed.

Current run against disposable Vaultwarden confirms every one of them verbatim:

── R1: linked custom field (type 3) poisons unrelated writes ──
  REPRODUCED R1 — any write aborts while a linked field exists (Unknown field type: 3)
── R2: named custom field preserved when creating a new item ──
  REPRODUCED R2 — value not readable via field=api_key after create (stored in login.password instead)
── R3: type-specific default on update (secure note) ──
  REPRODUCED R3 — set wrote a password field; get still returns 'old-note-value'
── R4: case-insensitive field matching on update ──
  REPRODUCED R4 — update added a duplicate field; get still returns 'old-value'

Findings fixed: 0 · reproduced: 4

So @ashebanow has an executable checklist for those four (RUN_REGRESSIONS=1 tests/vaultwarden_harness.sh) — they'll flip to FIXED as the fixes land. The collection-filtering finding needs an org + two collections in the fixture account; happy to extend the bootstrap for that as a follow-up if useful. On the self-hosted config finding: the only supported mechanism the CLI offers is bw config server (which requires being logged out), so the provider likely needs to shell that out — or document it as a user prerequisite, which is what Kamal's bitwarden adapter effectively does.

@ashebanow

ashebanow commented Jul 24, 2026 via email

Copy link
Copy Markdown
Author

@domenkozar

Copy link
Copy Markdown
Member

@Tails I'm planning to do a 0.17 release on Monday if you'd like to get this in. No issue if we target 0.18.

test: fully-automated integration run against disposable Vaultwarden
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants